在 Flutter App 中常常需要使用網路服務或者等待檔案系統回應,當我們使用這類服務時,整體的 UI 並不會阻塞。這些服務通常使用 async function
處理資料,並回傳 Future 物件。為了讓 UI 能在資料取得中與取得後切換,我們可以用 FutureBuilder
來建構畫面。FutureBuilder
在取得資料前及取得資料後會自動切換狀態,可以避免使用複雜的狀態管理。
範例程式傳送門: https://github.com/ksw2000/ironman-2024/tree/master/flutter-practice/future_builder
FutureBuilder
本身也是一個 StatefulWidget
。首先我們先了解如何建構它
class FutureBuilder<T> extends StatefulWidget {
const FutureBuilder({
super.key,
required this.future,
this.initialData,
required this.builder,
});
final Future<T>? future;
final AsyncWidgetBuilder<T> builder;
final T? initialData;
// ...
}
future
參數要代入一個 Future
物件,可以儲放向網路或檔案系統等異步請求的結果,為了方便演示,我們這裡設定延遲 2 秒後自動回傳一個字串:
Future.delayed(const Duration(seconds: 2), () => "SOME THING");
builder
的部分的一個函式參數,其中 context
的與 build()
中的 context
功能一樣,另一個 snapshot
用來代表 future
回傳值目前的狀態
Widget Function(BuildContext context, AsyncSnapshot<T> snapshot)
整體的寫法如下
FutureBuilder<String>(
future: _fetch,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
List<Widget> children;
if (snapshot.hasData) {
// 當 _fetch 回傳值後
// 利用 snapshot.data 取得值
} else if (snapshot.hasError) {
// 當 _fetch 拋出錯誤 (throw error) 時
// 利用 snaphost.error 取得值
} else {
// 初始狀態,通常我們會在這邊放一個轉圈圈 widget
}
}
)
有一點要注意的是:future
內的 Future 物件,必需在 build
之前被確定。也就是說我們不可以在 build()
才取得 Future 物件。如果每次 future
都和 FutureBuilder 同時建構,那麼當每次 FutureBuilder 的上層 Widget 被重建時,異步處理也會重新啟動。
// 錯誤範例
class UserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var _future = ...
return Scaffold(
//...
FutureBuilder(
future: _future,
builder: (context, snapshot) {
// ...
}
)
我們可以利用上方的模版來設計畫面。延續昨天的進度,當我們登入後,也許還會需要向伺服器請求資料,因此我們可以在 UserPage
中,加入 FutuerBuilder
class UserPage extends StatelessWidget {
UserPage({super.key});
final Future<String> _fetch =
Future.delayed(const Duration(seconds: 2), () => "SOME THING");
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('User Page'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: FutureBuilder(
future: _fetch,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Hello ${UserDataLayer.of(context).user?.name}'),
const SizedBox(height: 10),
Text('${snapshot.data}')
],
);
} else if (snapshot.hasError) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child: Text('Error: ${snapshot.error}'),
),
]);
}
return const CircularProgressIndicator();
}))));
}
}
當 _fetch 正常回傳後執行的效果如下:
我們也可以模擬當錯誤發生時:
final Future<String> _fetchError =
Future.delayed(const Duration(seconds: 2), () {
throw Exception('ERROR OCCUR');
});
// ...
FutureBuilder(
future: _fetchError
後記:9/12 只寫了一點點,9/13努力補完🙌